PEAK_ASYM

Overview

The PEAK_ASYM function fits a variety of asymmetric and symmetric peak models to x-y data using nonlinear least squares optimization. Peak fitting is essential in spectroscopy, chromatography, signal processing, and any domain where data exhibits bell-shaped or pulse-like patterns that need to be characterized mathematically.

This implementation uses scipy.optimize.curve_fit, which applies the Levenberg-Marquardt algorithm (or trust region methods when bounds are specified) to minimize the sum of squared residuals between the model and the observed data. The function returns both the fitted parameter values and their standard errors, derived from the covariance matrix of the fit.

The function supports nine peak models:

  • Gaussian Peak variants (Area, Amplitude, and FWHM parameterizations): The classic symmetric bell curve described by f(x) = y_0 + A \exp\left(-\frac{(x-x_c)^2}{2w^2}\right). The Gaussian function appears throughout statistics, physics, and signal processing due to the central limit theorem.

  • Asymmetric Double Sigmoidal Peak: Combines two logistic (sigmoidal) functions to create an asymmetric peak with independent rise and fall characteristics, useful for peaks with different leading and trailing edge behaviors.

  • Logistic Peak (Hubbert Curve): Based on the logistic distribution, this model produces a symmetric peak with heavier tails than a Gaussian. Originally developed by M. King Hubbert for modeling resource depletion, it is defined as f(x) = y_0 + \frac{4A e^{-(x-x_c)/w}}{(1 + e^{-(x-x_c)/w})^2}.

  • Power Symmetric/Asymmetric: Power-law models where intensity scales as |x - x_c|^P, with variants supporting offsets, centered forms, and asymmetric dual exponents for different behaviors on each side of the center.

  • Exponential Pulse (Rise-Decay): Models pulses with an exponential rise followed by exponential decay, commonly used for transient signals in electronics and photophysics.

For each model, initial parameter guesses are computed automatically from the data characteristics (range, median, amplitude), and appropriate parameter bounds ensure physically meaningful results. For more information, see the SciPy curve_fit documentation and the SciPy GitHub repository.

This example function is provided as-is without any representation of accuracy.

Excel Usage

=PEAK_ASYM(xdata, ydata, peak_asym_model)
  • xdata (list[list], required): The xdata value
  • ydata (list[list], required): The ydata value
  • peak_asym_model (str, required): The peak_asym_model value

Returns (list[list]): 2D list [param_names, fitted_values, std_errors], or error string.

Examples

Example 1: Demo case 1

Inputs:

peak_asym_model xdata ydata
asymmetric_double_sigmoidal_peak 0.01 0.6006047045310022
2.0075 1.0737954138646513
4.005 0.43007294650211997
6.0024999999999995 0.06091388592739846
8 0.019487637769087587

Excel formula:

=PEAK_ASYM("asymmetric_double_sigmoidal_peak", {0.01;2.0075;4.005;6.0024999999999995;8}, {0.6006047045310022;1.0737954138646513;0.43007294650211997;0.06091388592739846;0.019487637769087587})

Expected output:

"non-error"

Example 2: Demo case 2

Inputs:

peak_asym_model xdata ydata
gaussian_peak_area_parameterized 0.01 0.03025502969436638
2.0075 1.898441420966237
4.005 0.03062760430597533
6.0024999999999995 0.01
8 0.015251619698276176

Excel formula:

=PEAK_ASYM("gaussian_peak_area_parameterized", {0.01;2.0075;4.005;6.0024999999999995;8}, {0.03025502969436638;1.898441420966237;0.03062760430597533;0.01;0.015251619698276176})

Expected output:

"non-error"

Example 3: Demo case 3

Inputs:

peak_asym_model xdata ydata
gaussian_peak_amplitude_parameterized 0.01 0.7259943748121195
2.0075 2.7302902790530585
4.005 0.31525061168113216
6.0024999999999995 0.01
8 0.02066777122773472

Excel formula:

=PEAK_ASYM("gaussian_peak_amplitude_parameterized", {0.01;2.0075;4.005;6.0024999999999995;8}, {0.7259943748121195;2.7302902790530585;0.31525061168113216;0.01;0.02066777122773472})

Expected output:

"non-error"

Example 4: Demo case 4

Inputs:

peak_asym_model xdata ydata
gaussian_peak_fwhm_parameterized 0.01 0.025571345980890503
2.0075 2.1338126889403566
4.005 0.034234664064067055
6.0024999999999995 0.01
8 0.01715983261300243

Excel formula:

=PEAK_ASYM("gaussian_peak_fwhm_parameterized", {0.01;2.0075;4.005;6.0024999999999995;8}, {0.025571345980890503;2.1338126889403566;0.034234664064067055;0.01;0.01715983261300243})

Expected output:

"non-error"

Example 5: Demo case 5

Inputs:

peak_asym_model xdata ydata
logistic_peak_hubbert_curve 0.01 1.5079112140631585
2.0075 2.7688893016375915
4.005 1.0697838701118711
6.0024999999999995 0.14630301232765053
8 0.04847030906827912

Excel formula:

=PEAK_ASYM("logistic_peak_hubbert_curve", {0.01;2.0075;4.005;6.0024999999999995;8}, {1.5079112140631585;2.7688893016375915;1.0697838701118711;0.14630301232765053;0.04847030906827912})

Expected output:

"non-error"

Example 6: Demo case 6

Inputs:

peak_asym_model xdata ydata
power_symmetric_with_offset 0.1 46.51368448781824
1.3250000000000002 25.35817972099038
2.5500000000000003 17.794906308746665
3.7750000000000004 80.56414349732965
5 1066.128702416852

Excel formula:

=PEAK_ASYM("power_symmetric_with_offset", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {46.51368448781824;25.35817972099038;17.794906308746665;80.56414349732965;1066.128702416852})

Expected output:

"non-error"

Example 7: Demo case 7

Inputs:

peak_asym_model xdata ydata
power_symmetric_centered 0.1 46.51368448781824
1.3250000000000002 25.35817972099038
2.5500000000000003 17.794906308746665
3.7750000000000004 80.56414349732965
5 1066.128702416852

Excel formula:

=PEAK_ASYM("power_symmetric_centered", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {46.51368448781824;25.35817972099038;17.794906308746665;80.56414349732965;1066.128702416852})

Expected output:

"non-error"

Example 8: Demo case 8

Inputs:

peak_asym_model xdata ydata
power_asymmetric_dual_exponent 0.1 46.51368448781824
1.3250000000000002 25.35817972099038
2.5500000000000003 17.794906308746665
3.7750000000000004 80.56414349732965
5 1066.128702416852

Excel formula:

=PEAK_ASYM("power_asymmetric_dual_exponent", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {46.51368448781824;25.35817972099038;17.794906308746665;80.56414349732965;1066.128702416852})

Expected output:

"non-error"

Example 9: Demo case 9

Inputs:

peak_asym_model xdata ydata
exp_pulse_rise_decay 0.01 0.01
2.0075 0.01
4.005 0.06168147075764583
6.0024999999999995 0.13906182126049085
8 0.17613759877451055

Excel formula:

=PEAK_ASYM("exp_pulse_rise_decay", {0.01;2.0075;4.005;6.0024999999999995;8}, {0.01;0.01;0.06168147075764583;0.13906182126049085;0.17613759877451055})

Expected output:

"non-error"

Python Code

import numpy as np
from scipy.optimize import curve_fit as scipy_curve_fit
import math

def peak_asym(xdata, ydata, peak_asym_model):
    """
    Fits peak_asym models to data using scipy.optimize.curve_fit. See https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html for details.

    See: https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html

    This example function is provided as-is without any representation of accuracy.

    Args:
        xdata (list[list]): The xdata value
        ydata (list[list]): The ydata value
        peak_asym_model (str): The peak_asym_model value Valid options: Asymmetric Double Sigmoidal Peak, Gaussian Peak Area Parameterized, Gaussian Peak Amplitude Parameterized, Gaussian Peak Fwhm Parameterized, Logistic Peak Hubbert Curve, Power Symmetric With Offset, Power Symmetric Centered, Power Asymmetric Dual Exponent, Exp Pulse Rise Decay.

    Returns:
        list[list]: 2D list [param_names, fitted_values, std_errors], or error string.
    """
    def _validate_data(xdata, ydata):
        """Validate and convert both xdata and ydata to numpy arrays."""
        for name, arg in [("xdata", xdata), ("ydata", ydata)]:
            if not isinstance(arg, list) or len(arg) < 2:
                raise ValueError(f"{name}: must be a 2D list with at least two rows")
            vals = []
            for i, row in enumerate(arg):
                if not isinstance(row, list) or len(row) == 0:
                    raise ValueError(f"{name} row {i}: must be a non-empty list")
                try:
                    vals.append(float(row[0]))
                except Exception:
                    raise ValueError(f"{name} row {i}: non-numeric value")
            if name == "xdata":
                x_arr = np.asarray(vals, dtype=np.float64)
            else:
                y_arr = np.asarray(vals, dtype=np.float64)

        if x_arr.shape[0] != y_arr.shape[0]:
            raise ValueError("xdata and ydata must have the same number of rows")
        return x_arr, y_arr

    # Model definitions dictionary
    models = {
        'asymmetric_double_sigmoidal_peak': {
            'params': ['y0', 'xc', 'A', 'w1', 'w2', 'w3'],
            'model': lambda x, y0, xc, A, w1, w2, w3: y0 + A / (1.0 + np.exp(-(x - xc + 0.5 * w1) / w2)) * (1.0 - 1.0 / (1.0 + np.exp(-(x - xc - 0.5 * w1) / w3))),
            'guess': lambda xa, ya: (float(np.min(ya)), float(np.median(xa)), float(np.ptp(ya) if np.ptp(ya) else 1.0), float(max((np.max(xa) - np.min(xa)) / 4.0, 1e-3)), float(max((np.max(xa) - np.min(xa)) / 10.0, 1e-3)), float(max((np.max(xa) - np.min(xa)) / 10.0, 1e-3))),
            'bounds': ([-np.inf, -np.inf, -np.inf, 0.0, 0.0, 0.0], np.inf),
        },
        'gaussian_peak_area_parameterized': {
            'params': ['y0', 'xc', 'w', 'A'],
            'model': lambda x, y0, xc, w, A: y0 + (A / (w * np.sqrt(np.pi / 2.0))) * np.exp(-2.0 * np.power(x - xc, 2) / np.power(w, 2)),
            'guess': lambda xa, ya: (float(min(ya)), float(np.median(xa)), float(max((np.max(xa) - np.min(xa)) / 4.0, 1e-3)), float(max(ya))),
            'bounds': ([-np.inf, -np.inf, 0.0, -np.inf], np.inf),
        },
        'gaussian_peak_amplitude_parameterized': {
            'params': ['y0', 'xc', 'w', 'A'],
            'model': lambda x, y0, xc, w, A: y0 + A * np.exp(-np.power(x - xc, 2) / (2.0 * w * w)),
            'guess': lambda xa, ya: (float(min(ya)), float(np.median(xa)), 1.0, float(max(ya) - min(ya))),
            'bounds': ([-np.inf, -np.inf, 0.0, -np.inf], np.inf),
        },
        'gaussian_peak_fwhm_parameterized': {
            'params': ['y0', 'xc', 'A', 'w'],
            'model': lambda x, y0, xc, A, w: y0 + (A / (w * np.sqrt(np.pi / (4.0 * np.log(2.0))))) * np.exp(-4.0 * np.log(2.0) * np.square(x - xc) / np.square(w)),
            'guess': lambda xa, ya: (float(np.min(ya)), float(np.median(xa)), float(np.ptp(ya) if np.ptp(ya) else 1.0), float(max((np.max(xa) - np.min(xa)) / 4.0, 1e-3))),
            'bounds': ([-np.inf, -np.inf, -np.inf, 0.0], np.inf),
        },
        'logistic_peak_hubbert_curve': {
            'params': ['y0', 'xc', 'w', 'A'],
            'model': lambda x, y0, xc, w, A: y0 + 4.0 * A * np.exp(-(x - xc) / w) / np.power(1.0 + np.exp(-(x - xc) / w), 2),
            'guess': lambda xa, ya: (float(np.min(ya)), float(np.median(xa)), float(max((np.max(xa) - np.min(xa)) / 6.0, 1e-3)), float(np.ptp(ya) if np.ptp(ya) else 1.0)),
            'bounds': ([-np.inf, -np.inf, 0.0, -np.inf], np.inf),
        },
        'power_symmetric_with_offset': {
            'params': ['y0', 'xc', 'A', 'P'],
            'model': lambda x, y0, xc, A, P: y0 + A * np.power(np.abs(x - xc), P),
            'guess': lambda xa, ya: (float(np.min(ya)), float(np.median(xa)), float(np.ptp(ya) if np.ptp(ya) else 1.0), 1.0),
            'bounds': ([-np.inf, -np.inf, 0.0, 0.0], np.inf),
        },
        'power_symmetric_centered': {
            'params': ['xc', 'A', 'P'],
            'model': lambda x, xc, A, P: A * np.power(np.abs(x - xc), P),
            'guess': lambda xa, ya: (float(np.median(xa)), float(np.ptp(ya) if np.ptp(ya) else 1.0), 1.0),
            'bounds': ([-np.inf, 0.0, 0.0], np.inf),
        },
        'power_asymmetric_dual_exponent': {
            'params': ['xc', 'A', 'pl', 'pu'],
            'model': lambda x, xc, A, pl, pu: np.where(x < xc, A * np.power(np.abs(x - xc), pl), A * np.power(np.abs(x - xc), pu)),
            'guess': lambda xa, ya: (float(np.median(xa)), float(np.ptp(ya) if np.ptp(ya) else 1.0), 1.0, 1.5),
            'bounds': ([-np.inf, 0.0, 0.0, 0.0], np.inf),
        },
        'exp_pulse_rise_decay': {
            'params': ['y0', 'x0', 'A', 't1', 'P', 't2'],
            'model': lambda x, y0, x0, A, t1, P, t2: np.where(x >= x0, y0 + A * np.power(1.0 - np.exp(-(x - x0) / t1), P) * np.exp(-(x - x0) / t2), y0),
            'guess': lambda xa, ya: (float(np.min(ya)), float(np.median(xa)), float(np.ptp(ya) if np.ptp(ya) else 1.0), float(max((np.max(xa) - np.min(xa)) / 10.0, 1e-3)), 2.0, float(max((np.max(xa) - np.min(xa)) / 10.0, 1e-3))),
            'bounds': ([-np.inf, -np.inf, 0.0, 0.0, 0.0, 0.0], np.inf),
        }
    }

    # Validate model parameter
    if peak_asym_model not in models:
        return f"Invalid model: {str(peak_asym_model)}. Valid models are: {', '.join(models.keys())}"

    model_info = models[peak_asym_model]

    # Validate and convert input data
    try:
        x_arr, y_arr = _validate_data(xdata, ydata)
    except ValueError as e:
        return f"Invalid input: {e}"

    # Perform curve fitting
    try:
        p0 = model_info['guess'](x_arr, y_arr)
        bounds = model_info.get('bounds', (-np.inf, np.inf))
        if bounds == (-np.inf, np.inf):
            popt, pcov = scipy_curve_fit(model_info['model'], x_arr, y_arr, p0=p0, maxfev=10000)
        else:
            popt, pcov = scipy_curve_fit(model_info['model'], x_arr, y_arr, p0=p0, bounds=bounds, maxfev=10000)

        fitted_vals = [float(v) for v in popt]
        for v in fitted_vals:
            if math.isnan(v) or math.isinf(v):
                return "Fitting produced invalid numeric values (NaN or inf)."
    except ValueError as e:
        return f"Initial guess error: {e}"
    except Exception as e:
        return f"curve_fit error: {e}"

    # Calculate standard errors
    std_errors = None
    try:
        if pcov is not None and np.isfinite(pcov).all():
            std_errors = [float(v) for v in np.sqrt(np.diag(pcov))]
    except Exception:
        pass

    return [model_info['params'], fitted_vals, std_errors] if std_errors else [model_info['params'], fitted_vals]

Online Calculator